Skip to content

fix: table toolbar, header columns, click-to-select, wrap and auto-fit (#556) - #557

Merged
vibhavkatre merged 8 commits into
frappe:mainfrom
bvnaik05:fix/table-toolbar-ux-556
Sep 1, 2026
Merged

vibhavkatre merged 8 commits into
frappe:mainfrom
bvnaik05:fix/table-toolbar-ux-556

Conversation

@bvnaik05

@bvnaik05 bvnaik05 commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

All 18 items of #556 except one known pre-existing gap (see below):

  • Combines Edit Table / Table Actions into one icon-only Table menu; Merge Cells stays separate, icon-only.
  • Unifies table- and cell-level text colour into one Text Colour control ("A" glyph, matching the text-box control).
  • Adds Fill and Border controls for cells/table, matching Shape Fill/Border.
  • Delete moves to the end of the toolbar, separated, red.
  • Adds a font selector for table cell text.
  • Adds header columns, independently configurable alongside header rows.
  • Single click on a picked table selects a cell rather than opening it for editing; only a double click opens it. Drag-range selection and click-outside deselection are unchanged (whiteboard-only documents — see gap below for unified documents).
  • Cell text wraps instead of scrolling; a row grows live as it's typed into, with growth and text landing in one undo step.
  • Double-clicking a column or row's resize handle auto-fits that dimension to its content.
  • Wrapping is mark-aware (bold/italic/underline/strike stay on the right characters after a wrap), and the committed render + export mirror the live editor.

Known gap — not introduced by this branch

Clicking empty canvas does not close/deselect an open table on a unified document (the type "Create" makes, i.e. most new diagrams). Root cause: useSelection.js's empty-click path (the shared block-shape selection) never reaches the whiteboard UI's own clearSelection, which is what actually clears editingCell/cellRange — a gap in the unified-canvas click-dispatch architecture shared by every whiteboard object type (stickies, lines, strokes), not just tables. Reproduces identically on main before this branch, confirmed by stashing this branch's commits and rebuilding. Left unfixed here rather than risk a rushed change to click dispatch shared across the whole canvas — happy to take it as a fast follow-up if wanted.

Everything else in #556's acceptance criteria is implemented and covered by tests below.

Test plan

  • yarn vitest run — 1765 tests passing
  • yarn build / yarn lint — clean
  • Manually verified in the running app: table placement, the combined Table menu, Text Colour / Fill / Border popovers, double-click-to-edit, header row + header column independently, long text wrapping with no scrollbar and live row growth, double-click auto-fit.
  • Click-outside-deselect on a unified document — confirmed broken, tracked above, not fixed in this PR.

🤖 Generated with Claude Code

…ct cells (frappe#556)

Combines the separate Edit Table / Table Actions controls into one icon-only
Table menu, unifies table- and cell-level text colour into a single Text
Colour control, adds Fill and Border to match Shape formatting, and moves
Delete to the end of the toolbar. Adds header columns alongside header rows,
independently configurable. A single click on a picked table now selects a
cell instead of opening it for editing — only a double click opens it — with
click+drag range selection and click-outside deselection unchanged. Adds a
font selector for table cell text, reusing the shared Espresso font list now
extracted into diagram/textFonts.js.

Auto-fit on double-clicking a resize handle and cell text wrapping (no
scrollbars, row auto-grow) are not yet in this PR — tracked as the remaining
slices of frappe#556.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 19, 2026 11:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

bvnaik05 and others added 2 commits August 19, 2026 11:47
…#556)

Lint: canMerge/canSplit in WhiteboardTable.vue and extended in
startCellRangeDrag were dead once showRange stopped reading them and the
click-vs-drag release branch was removed.

E2E: table-cell-text.spec.js opened a cell with two separate clicks (the old
T2 path); a plain click now only selects, so opening a cell needs a double
click. Also repoints the cell-colour test at the unified Text Colour control
("Cell text colour" was folded into it).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… auto-fit (frappe#556)

A cell now wraps its text instead of scrolling horizontally, and its row grows
live as it's typed into — mirroring WhiteboardStickyNote's growToText/commit
split (unrecorded growth while typing, the final size landing with the text in
one undo step). Enter inserts a line break instead of committing, reusing the
same newlineIntent list-continuation the sticky note uses; commit now happens
on click-away, matching how a sticky note behaves.

Adds a mark-aware run wrapper (wrapRuns, richText.js) so a wrapped cell keeps
its bold/italic/underline/strike marks on the exact characters they belonged
to — including the original whitespace's own marks, not a reconstructed
space that could silently move a formatting boundary. The committed SVG
render and the export mirror the same wrapped, marked lines.

Double-clicking a column or row's resize handle auto-fits that dimension to
its content, reusing the same undo labels a manual drag already uses.

lineBeforeCaret/deleteBeforeCaret move from WhiteboardStickyNote.vue into
richTextDom.js so the table cell editor can reuse them rather than holding a
second copy.

Known gap, not introduced by this change: clicking empty canvas does not
close/deselect an open table on a UNIFIED document (most new diagrams) — the
select tool's empty-click path there (useSelection.js) never reaches the
whiteboard UI's own clearSelection, which is what actually clears editingCell/
cellRange. Reproduces identically on main before this branch's changes, so it
predates frappe#556 entirely. Filing separately rather than risking a rushed fix to
shared click-dispatch code shared by every whiteboard object type, not just
tables.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@bvnaik05 bvnaik05 changed the title fix: consolidate the table toolbar, header columns, and click-to-select cells (#556) fix: table toolbar, header columns, click-to-select, wrap and auto-fit (#556) Aug 19, 2026
bvnaik05 and others added 3 commits August 19, 2026 13:48
…#556)

Enter now inserts a line break in a wrapping cell instead of closing it, so
table-cell-text.spec.js's commit step (previously page.keyboard.press('Enter'))
never landed. Replaced with a click on the seeded table's neighbouring cell,
which reliably commits (startCellRangeDrag nulls editingCell on every press,
whichever cell it lands on) without depending on the separate, currently-
broken empty-canvas deselect path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
frappe#556)

boxInWindow(page, cell, ...) returns the "CELL-TEXT" <text> element's own
rendered bounding box — as wide as the glyphs (~60-90px) — not the table
cell's (120-160px in these fixtures). A 1.5x multiple of that box never left
the originating cell, so the click landed on the still-open editor's own div
(pointerdown.stop) instead of the table underneath, and nothing committed.
Fixed to a flat 200px offset from the text's left edge, which clears both
fixtures' cell widths into the neighbouring column.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…up (frappe#556)

Feedback on the combined Table control:
- Its icon (lucide-table) was the same one the Table INSERT tool and the
  toolbar's Table trigger area already use — swapped to lucide-table-properties
  so it reads as "settings", not "insert" or "the Merge icon's neighbour".
- Moved it next to Merge/Split by rendering TableCellGroup before
  WhiteboardObjectGroup in CanvasToolbar.vue, so the cell group's trailing
  Merge/Split sits immediately before the object group's leading Table
  control — both reshape the table, just gated differently. Delete stays the
  true last item, at the end of the object group either way.
- The popup ran off the bottom of the screen: a single narrow (176px) column
  stacked Rows/Columns/header checkboxes/alignment/the action menu one item
  per line. Widened to 320px (w-72, matching TableOptions) and laid Rows +
  Columns, the two header checkboxes, and the action menu's own groups out
  side by side, roughly halving the popup's height.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@vibhavkatre vibhavkatre left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the whole branch against the source. The toolbar consolidation, header columns, click-to-select and double-click auto-fit all hold up, and the mark-aware wrapRuns is a careful piece of work — keeping the original whitespace character instead of reconstructing it is the right call. One confirmed defect before this can merge.

The sizing wrap and the render wrap are not the same algorithm

richText.js says wrapCharLine is the "same algorithm as textMetrics.js's wrapLines, so a cell wraps exactly where the height that was measured for it expects it to". They diverge on consecutive whitespace, and the render always produces the extra line:

  • wrapLines splits on /\s+/ and drops empties, so a run of whitespace costs one column.
  • wrapCharLine keeps every original whitespace character as its own token, so a run of n whitespace characters costs n columns.

The sizing path — wrappedCellLines → wrappedCellHeight, which feeds growTableRow, flushDraft and autoFitRowHeight — goes through wrapLines. The canvas render (wrappedCellRunLines) and the export (useThumbnail.js) go through wrapRuns. So a cell holding two adjacent spaces is grown for fewer lines than it is drawn with, and the last line falls outside the row.

Reproduces on a default table (TABLE_CELL_W 120, TABLE_FONT_SIZE 14, so perLine is 12):

"Total:  1,240"
  row grown for 1 line : ["Total: 1,240"]
  canvas draws   2 lines: ["Total:  ", "1,240"]

"one   two   three   four"   (perLine 20)
  row grown for 1 line : ["one two three four"]
  canvas draws   2 lines: ["one   two   three   ", "four"]

Two spaces after a full stop is ordinary typing, so this is reachable without anything unusual in the cell. trimRuns does not help — it only trims the ends, and the divergence is interior.

The intent behind wrapCharLine is right, so the fix probably belongs on the sizing side: measure with the same per-character tokenizer rather than with wrapLines, so both paths count whitespace the same way. Then the comment's claim becomes true, and the export, the canvas and the row height agree by construction rather than by coincidence.

Worth a test that pins the two paths against each other over text with double spaces, so they cannot drift apart again.

Minor

The known gap you flagged in the commit message — clicking empty canvas does not deselect an open table on a unified document — has no issue filed for it that I can find. Please file it so it is not lost.

Verified, no action needed

  • ItemListRow and Popover's toggle default-slot prop both exist in frappe-ui 1.0.0-beta.19.
  • border in WhiteboardObjectGroup cannot be undefined: hasSelection is cells.length > 0, so firstCell is always present when it is true.
  • charsPerLine clamps to 1, so a 24px column cannot drive wrapCharLine's long-word loop into a hang.
  • No unused imports left after the refactor.

bvnaik05 and others added 2 commits August 27, 2026 07:37
…ppe#556, frappe#557)

wrappedCellLines sized a row's height with textMetrics.js's wrapLines, which
collapses a whole run of whitespace to one column via /\s+/ — but the canvas
render and export (wrappedCellRunLines, via richText.js's wrapRuns) count
every whitespace character as its own column. A cell with a run of two or
more consecutive spaces was grown for fewer lines than it was drawn with, so
its last line fell outside the row. wrappedCellLines now goes through the
same per-character wrapRuns tokenizer, so sizing and rendering agree by
construction. Filed frappe#563 for the separate empty-canvas-deselect
gap the review also flagged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 28, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

The canceled-edit row-growth defect should be fixed before merging.

Typing can mutate row geometry outside history, while Escape discards only the text draft and leaves that geometry applied.

Files Needing Attention: frontend/src/composables/useTableCellEditor.js

Prompt To Fix All With AI
### Issue 1
frontend/src/composables/useTableCellEditor.js:118-120
**Escape preserves row growth**

When typing grows a table row and the user presses Escape, the text draft is discarded but the unrecorded `growTableRow` mutation is not reverted, leaving the row permanently enlarged without an undo entry.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "Merge branch 'main' into fix/table-toolb..." | Re-trigger Greptile

Comment on lines +118 to +120
updateLineCount(draftCell.value, runs)
const current = toValue(table)
store.growTableRow(current.id, draftCell.value.row, measuredHeight(draftCell.value, runs))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Escape preserves row growth

When typing grows a table row and the user presses Escape, the text draft is discarded but the unrecorded growTableRow mutation is not reverted, leaving the row permanently enlarged without an undo entry.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/composables/useTableCellEditor.js
Line: 118-120

Comment:
**Escape preserves row growth**

When typing grows a table row and the user presses Escape, the text draft is discarded but the unrecorded `growTableRow` mutation is not reverted, leaving the row permanently enlarged without an undo entry.

**Knowledge Base Used:**
- [Canvas interaction and rendering](https://app.greptile.com/frappe/-/custom-context/knowledge-base/frappe/draw/-/docs/canvas-interaction-and-rendering.md)
- [Whiteboard and freeform content](https://app.greptile.com/frappe/-/custom-context/knowledge-base/frappe/draw/-/docs/whiteboard-and-freeform-content.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@vibhavkatre vibhavkatre left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wrap defect is fixed at the right layer. wrappedCellLines now goes through wrapRuns' per-character tokenizer, so sizing and rendering share one definition of what a run of whitespace costs instead of two that disagreed — wrapLines' /\s+/ charging one column where the render charged one per character. Sizing and drawing now agree by construction rather than by coincidence, which is the fix I was after.

The new test compares the two paths directly on the repro ('one two three four' at 120px), so it fails if they ever diverge again. The comments in both files were updated to describe the new arrangement rather than left pointing at the old one.

Merging.

@vibhavkatre
vibhavkatre merged commit 2bb1e6e into frappe:main Sep 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants